/*
MIT License

Copyright (c) 2022 МГТУ им. Н.Э. Баумана, кафедра ИУ-6, Михаил Фетисов,

https://bmstu.codes/lsx/simodo/loom
*/

#include "ScriptSemantics_abstract.h"
#include "simodo/interpret/AnalyzeException.h"

#include "simodo/inout/convert/functions.h"
#include "simodo/inout/format/fmt.h"

#include <cassert>

namespace simodo::interpret
{
    namespace
    {
        // static const variable::Variable error = variable::error_variable();
    }

    void ScriptSemantics_abstract::appendContractProperties(const variable::Variable & from, variable::Variable & to)
    {
        /// \note Спецификация (Variable::spec) передаётся переменной только слева направо, т.е. не через
        /// инициализацию и не через присвоение.
        /// Информация о типе хранится только в спецификации (Variable::spec), а значит, если она была задана 
        /// в структуре, то её нужно перенести в спеку контракта.

        const variable::Variable &              from_origin  = from.origin();
        const std::shared_ptr<variable::Object> from_spec    = from_origin.spec().object();
        const bool                              spec_in_body = from_origin.value().isObject()
                                                            && from_spec->find(variable::SPEC_ORIGIN).isString() 
                                                            && from_spec->find(variable::SPEC_ORIGIN).getString() 
                                                                == variable::SPEC_ORIGIN_STRUCTURE;

        const std::shared_ptr<variable::Object> spec_source = spec_in_body 
                                                    ? from_origin.value().getObject()
                                                    : from_spec;

        if (!spec_in_body) {
            if (from_origin.value().isObject()
             && (!to.spec().object()->getVariableByName(SPEC_NAME_INITIAL_VALUE).name().empty() 
              || !to.spec().object()->getVariableByName(SPEC_NAME_BUILT_IN_TYPE).name().empty()))
                throw AnalyzeException( "ScriptSemantics_abstract::appendContractProperties", 
                                        from.location().makeLocation(inter().files()), 
                                        inout::fmt("Invalid addition of a non-scalar type to a scalar sequence"));

            if (to.type() != variable::ValueType::Null
             && from_origin.type() != variable::ValueType::Null)
                if (to.type() != from_origin.type() || from_origin.value().isArray())
                    throw AnalyzeException( "ScriptSemantics_abstract::appendContractProperties", 
                                            from.location().makeLocation(inter().files()), 
                                            inout::fmt("Multiple addition of a non-scalar type"));

            if (from_origin.value().isObject()) {
                // Наполняем структуру типа объекта
                if (to.type() == variable::ValueType::Null)
                    to.value() = from_origin.value();
                else if (to.value().isObject()) {
                    std::shared_ptr<variable::Object> to_object = to.value().getObject();

                    for(const variable::Variable & p : from_origin.value().getObject()->variables()) 
                        if (to_object->getVariableByName(p.name()).name().empty())
                            to_object->variables().push_back(p);
                        else
                            to_object->find(p.name()) = p.value();
                }
            }
            else if (from_origin.value().isArray())
                to.value() = from_origin.value();
        }

        // Наполняем структуру спецификации
        for(const variable::Variable & spec : spec_source->variables()) {
            if (spec.name() == variable::SPEC_ORIGIN) {
                // Происхождение спецификации не имеет значения
                continue;
            }

            // Очевидные проверки
            inout::TokenLocation err_loc = spec_in_body ? spec.location() : from.location();

            if ((spec.name() == SPEC_NAME_INITIAL_VALUE && !spec.value().isNull())
             || spec.name() == SPEC_NAME_BUILT_IN_TYPE) {
                if (to.value().isObject())
                    throw AnalyzeException( "ScriptSemantics_abstract::appendContractProperties", 
                                            err_loc.makeLocation(inter().files()), 
                                            inout::fmt("Invalid addition of a scalar type to a non-scalar sequence"));

                if (!to.spec().object()->getVariableByName(SPEC_NAME_INITIAL_VALUE).name().empty() 
                 || !to.spec().object()->getVariableByName(SPEC_NAME_BUILT_IN_TYPE).name().empty())
                    throw AnalyzeException( "ScriptSemantics_abstract::appendContractProperties", 
                                            err_loc.makeLocation(inter().files()), 
                                            inout::fmt("Type overriding"));
            }
            if (spec.name() == SPEC_NAME_BUILT_IN_TYPE) {
                int64_t built_it_type = spec.value().getInt();
                if (built_it_type < static_cast<int64_t>(variable::ValueType::Null) 
                 || built_it_type > static_cast<int64_t>(variable::ValueType::Array))
                    throw AnalyzeException( "ScriptSemantics_abstract::appendContractProperties", 
                                            err_loc.makeLocation(inter().files()), 
                                            inout::fmt("Invalid built-in type value"));
            }
            const variable::Variable & existing_spec = to.spec().object()->getVariableByName(spec.name());
            /// \todo Проверять на неравенство!
            if (!existing_spec.name().empty())
                throw AnalyzeException( "ScriptSemantics_abstract::appendContractProperties", 
                                        err_loc.makeLocation(inter().files()), 
                                        inout::fmt("Duplication of specification '%1'")
                                            .arg(spec.name()));
            if (spec.name() == variable::SPEC_HIDDEN && spec.value().isBool() && spec.value().getBool()) { 
                if (stack().getTopBoundaryMarkAndScope().first != BOUNDARY_MARK_MODULE_FRAME)
                    throw AnalyzeException( "ScriptSemantics_abstract::appendContractProperties", 
                                            err_loc.makeLocation(inter().files()), 
                                            inout::fmt("The internal variables of the module are already hidden"));
            }

            // Копируем спецификацию (std::shared_ptr для объектов и массивов сохраняются)
            to.spec().set(spec.name(),spec.value());
        }
    }

    variable::Variable ScriptSemantics_abstract::createVariable( const variable::Variable & contract, 
                                                const inout::Token & token, 
                                                const std::u16string & origin)
    {
        variable::Variable var { token.lexeme(), {}, token.location(), 
                contract.spec().isNull() 
                    ? variable::Specification {} 
                    : variable::Specification { contract.spec().object()->copy() } };

        auto it_origin_spec = std::find_if(
                                        contract.spec().object()->variables().begin(),
                                        contract.spec().object()->variables().end(),
                                        [](const variable::Variable & v){ return v.name() == variable::SPEC_ORIGIN; });

        if (it_origin_spec != contract.spec().object()->variables().end())
            contract.spec().object()->variables().erase(it_origin_spec);

        if (contract.value().isObject()) // модуль
            var.value() = contract.value().copy(); // явное копирование - отвязка от контракта (иначе, работает std::shared_ptr для объектов и массивов)
        else if (contract.value().isArray()) 
            var.value() = contract.value().copy();
        else {
            if (!contract.spec().object()->getVariableByName(SPEC_NAME_INITIAL_VALUE).name().empty())
                var.value() = contract.spec().object()->find(SPEC_NAME_INITIAL_VALUE);
            else if (!contract.spec().object()->getVariableByName(SPEC_NAME_BUILT_IN_TYPE).name().empty())
                var.value() = {variable::ValueType(contract.spec().object()->find(SPEC_NAME_BUILT_IN_TYPE).getInt())};
            else
                throw AnalyzeException( "ScriptSemantics_abstract::createVariable", 
                                        token.location().makeLocation(inter().files()), 
                                        inout::fmt("Unable to create variable '%1' without type specification")
                                            .arg(token.lexeme()));
        }

        var.spec().set(variable::SPEC_ORIGIN, origin);

        return var;
    }

}